Scheduled
{{end}}{{if .Title.String}}diff --git a/README.md b/README.md index e63d63e..8ba94ab 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,133 @@
{{.SiteName}} is home to {{largeNumFmt .AboutStats.NumPosts}} {{pluralize "article" "articles" .AboutStats.NumPosts}} across {{largeNumFmt .AboutStats.NumBlogs}} {{pluralize "blog" "blogs" .AboutStats.NumBlogs}}.
{{end}}WriteFreely is a self-hosted, decentralized blogging platform for publishing beautiful, simple blogs.
It lets you publish a single blog, or host a community of writers who can create multiple blogs under one account. You can also enable federation, which allows people in the fediverse to follow your blog, bookmark your posts, and share them with others.
Registration is currently closed.
You can always sign up on another instance.
{{ end }}The fediverse is a large network of platforms that all speak a common language. Imagine if you could reply to Instagram posts from Twitter, or interact with your favorite Medium blogs from Facebook — federated alternatives like PixelFed, Mastodon, and Write Freely enable you to do these types of things.
+The fediverse is a large network of platforms that all speak a common language. Imagine if you could reply to Instagram posts from Twitter, or interact with your favorite Medium blogs from Facebook — federated alternatives like PixelFed, Mastodon, and WriteFreely enable you to do these types of things.
Write Freely can communicate with other federated platforms like Mastodon, so people can follow your blogs, bookmark their favorite posts, and boost them to their followers. Sign up above to create a blog and join the fediverse.
+WriteFreely can communicate with other federated platforms like Mastodon, so people can follow your blogs, bookmark their favorite posts, and boost them to their followers. Sign up above to create a blog and join the fediverse.
Last updated {{.Updated}}
{{.Content}}(.+)
") ) func (p *Post) formatContent(c *Collection, isOwner bool) { baseURL := c.CanonicalURL() if !isSingleUser { baseURL = "/" + c.Alias + "/" } p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String))) p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL)) if exc := strings.Index(string(p.Content), ""); exc > -1 { p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL)) } } func (p *PublicPost) formatContent(isOwner bool) { p.Post.formatContent(&p.Collection.Collection, isOwner) } func applyMarkdown(data []byte, baseURL string) string { return applyMarkdownSpecial(data, false, baseURL) } func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string) string { mdExtensions := 0 | blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | blackfriday.EXTENSION_AUTOLINK | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_SPACE_HEADERS | blackfriday.EXTENSION_AUTO_HEADER_IDS htmlFlags := 0 | blackfriday.HTML_USE_SMARTYPANTS | blackfriday.HTML_SMARTYPANTS_DASHES if baseURL != "" { htmlFlags |= blackfriday.HTML_HASHTAGS } // Generate Markdown md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) if baseURL != "" { // Replace special text generated by Markdown parser md = []byte(hashtagReg.ReplaceAll(md, []byte("#$1"))) } // Strip out bad HTML policy := getSanitizationPolicy() policy.RequireNoFollowOnLinks(!skipNoFollow) outHTML := string(policy.SanitizeBytes(md)) // Strip newlines on certain block elements that render with them outHTML = blockReg.ReplaceAllString(outHTML, "<$1>") outHTML = endBlockReg.ReplaceAllString(outHTML, "$1>$2>") // Remove all query parameters on YouTube embed links // TODO: make this more specific. Taking the nuclear approach here to strip ?autoplay=1 outHTML = youtubeReg.ReplaceAllString(outHTML, "$1") return outHTML } func applyBasicMarkdown(data []byte) string { mdExtensions := 0 | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_SPACE_HEADERS | blackfriday.EXTENSION_HEADER_IDS htmlFlags := 0 | blackfriday.HTML_SKIP_HTML | blackfriday.HTML_USE_SMARTYPANTS | blackfriday.HTML_SMARTYPANTS_DASHES // Generate Markdown md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) // Strip out bad HTML policy := bluemonday.UGCPolicy() policy.AllowAttrs("class", "id").Globally() outHTML := string(policy.SanitizeBytes(md)) outHTML = markeddownReg.ReplaceAllString(outHTML, "$1") outHTML = strings.TrimRightFunc(outHTML, unicode.IsSpace) return outHTML } func postTitle(content, friendlyId string) string { const maxTitleLen = 80 // Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML // entities added in by sanitizing the content. content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content)) content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace) eol := strings.IndexRune(content, '\n') blankLine := strings.Index(content, "\n\n") if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen { return strings.TrimSpace(content[:blankLine]) } else if utf8.RuneCountInString(content) <= maxTitleLen { return content } return friendlyId } // TODO: fix duplicated code from postTitle. postTitle is a widely used func we // don't have time to investigate right now. func friendlyPostTitle(content, friendlyId string) string { const maxTitleLen = 80 // Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML // entities added in by sanitizing the content. content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content)) content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace) eol := strings.IndexRune(content, '\n') blankLine := strings.Index(content, "\n\n") if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen { return strings.TrimSpace(content[:blankLine]) } else if eol == -1 && utf8.RuneCountInString(content) <= maxTitleLen { return content } title, truncd := parse.TruncToWord(parse.PostLede(content, true), maxTitleLen) if truncd { title += "..." } return title } func getSanitizationPolicy() *bluemonday.Policy { policy := bluemonday.UGCPolicy() policy.AllowAttrs("src", "style").OnElements("iframe", "video") policy.AllowAttrs("frameborder", "width", "height").Matching(bluemonday.Integer).OnElements("iframe") policy.AllowAttrs("allowfullscreen").OnElements("iframe") policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video") policy.AllowAttrs("target").OnElements("a") policy.AllowAttrs("style", "class", "id").Globally() policy.AllowURLSchemes("http", "https", "mailto", "xmpp") return policy } func sanitizePost(content string) string { return strings.Replace(content, "<", "<", -1) } // postDescription generates a description based on the given post content, // title, and post ID. This doesn't consider a V2 post field, `title` when // choosing what to generate. In case a post has a title, this function will // fail, and logic should instead be implemented to skip this when there's no // title, like so: // var desc string // if title == "" { // desc = postDescription(content, title, friendlyId) // } else { // desc = shortPostDescription(content) // } func postDescription(content, title, friendlyId string) string { maxLen := 140 if content == "" { - content = "Write Freely is a painless, simple, federated blogging platform." + content = "WriteFreely is a painless, simple, federated blogging platform." } else { fmtStr := "%s" truncation := 0 if utf8.RuneCountInString(content) > maxLen { // Post is longer than the max description, so let's show a better description fmtStr = "%s..." truncation = 3 } if title == friendlyId { // No specific title was found; simply truncate the post, starting at the beginning content = fmt.Sprintf(fmtStr, strings.Replace(stringmanip.Substring(content, 0, maxLen-truncation), "\n", " ", -1)) } else { // There was a title, so return a real description blankLine := strings.Index(content, "\n\n") if blankLine < 0 { blankLine = 0 } truncd := stringmanip.Substring(content, blankLine, blankLine+maxLen-truncation) contentNoNL := strings.Replace(truncd, "\n", " ", -1) content = strings.TrimSpace(fmt.Sprintf(fmtStr, contentNoNL)) } } return content } func shortPostDescription(content string) string { maxLen := 140 fmtStr := "%s" truncation := 0 if utf8.RuneCountInString(content) > maxLen { // Post is longer than the max description, so let's show a better description fmtStr = "%s..." truncation = 3 } return strings.TrimSpace(fmt.Sprintf(fmtStr, strings.Replace(stringmanip.Substring(content, 0, maxLen-truncation), "\n", " ", -1))) } diff --git a/templates.go b/templates.go index 802856f..0f93cb9 100644 --- a/templates.go +++ b/templates.go @@ -1,193 +1,193 @@ /* * Copyright © 2018 A Bunch Tell LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "github.com/dustin/go-humanize" "github.com/writeas/web-core/l10n" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" "html/template" "io" "io/ioutil" "net/http" "os" "path/filepath" "strings" ) var ( templates = map[string]*template.Template{} pages = map[string]*template.Template{} userPages = map[string]*template.Template{} funcMap = template.FuncMap{ "largeNumFmt": largeNumFmt, "pluralize": pluralize, "isRTL": isRTL, "isLTR": isLTR, "localstr": localStr, "localhtml": localHTML, "tolower": strings.ToLower, } ) const ( templatesDir = "templates" pagesDir = "pages" ) func showUserPage(w http.ResponseWriter, name string, obj interface{}) { if obj == nil { log.Error("showUserPage: data is nil!") return } if err := userPages[filepath.Join("user", name+".tmpl")].ExecuteTemplate(w, name, obj); err != nil { log.Error("Error parsing %s: %v", name, err) } } func initTemplate(parentDir, name string) { if debugging { log.Info(" " + filepath.Join(parentDir, templatesDir, name+".tmpl")) } files := []string{ filepath.Join(parentDir, templatesDir, name+".tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), } if name == "collection" || name == "collection-tags" { // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl")) } if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" { files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl")) } templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) } func initPage(parentDir, path, key string) { if debugging { log.Info(" [%s] %s", key, path) } pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles( path, filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), )) } func initUserPage(parentDir, path, key string) { if debugging { log.Info(" [%s] %s", key, path) } userPages[key] = template.Must(template.New(key).Funcs(funcMap).ParseFiles( path, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"), )) } func initTemplates(cfg *config.Config) error { log.Info("Loading templates...") tmplFiles, err := ioutil.ReadDir(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir)) if err != nil { return err } for _, f := range tmplFiles { if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") { parts := strings.Split(f.Name(), ".") key := parts[0] initTemplate(cfg.Server.TemplatesParentDir, key) } } log.Info("Loading pages...") // Initialize all static pages that use the base template filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, pagesDir), func(path string, i os.FileInfo, err error) error { if !i.IsDir() && !strings.HasPrefix(i.Name(), ".") { key := i.Name() initPage(cfg.Server.PagesParentDir, path, key) } return nil }) log.Info("Loading user pages...") // Initialize all user pages that use base templates filepath.Walk(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir, "user"), func(path string, f os.FileInfo, err error) error { if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") { corePath := path if cfg.Server.TemplatesParentDir != "" { corePath = corePath[len(cfg.Server.TemplatesParentDir)+1:] } parts := strings.Split(corePath, string(filepath.Separator)) key := f.Name() if len(parts) > 2 { key = filepath.Join(parts[1], f.Name()) } initUserPage(cfg.Server.TemplatesParentDir, path, key) } return nil }) return nil } // renderPage retrieves the given template and renders it to the given io.Writer. // If something goes wrong, the error is logged and returned. func renderPage(w io.Writer, tmpl string, data interface{}) error { err := pages[tmpl].ExecuteTemplate(w, "base", data) if err != nil { log.Error("%v", err) } return err } func largeNumFmt(n int64) string { return humanize.Comma(n) } func pluralize(singular, plural string, n int64) string { if n == 1 { return singular } return plural } func isRTL(d string) bool { return d == "rtl" } func isLTR(d string) bool { return d == "ltr" || d == "auto" } func localStr(term, lang string) string { s := l10n.Strings(lang)[term] if s == "" { s = l10n.Strings("")[term] } return s } func localHTML(term, lang string) template.HTML { s := l10n.Strings(lang)[term] if s == "" { s = l10n.Strings("")[term] } - s = strings.Replace(s, "write.as", "write freely", 1) + s = strings.Replace(s, "write.as", "writefreely", 1) return template.HTML(s) } diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index fe4cee8..06bfa67 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -1,128 +1,128 @@ {{define "post"}}Scheduled
{{end}}{{if .Title.String}}{{.Description}}
{{end}} {{/*if not .Public/*}} {{/*end*/}} {{if .PinnedPosts}} {{end}}This is your new blog.
Start writing, or customize your blog.
Check out our writing guide to see what else you can do, and get in touch anytime with questions or feedback.
{{.Message}}
{{end}}writefreely --reset-pass <username>
+ Read more in the configuration docs.
{{if .ConfigMessage}}{{.ConfigMessage}}
{{end}}Pages | +Page | Last Modified |
---|---|---|
{{.ID}} | +{{if .Title.Valid}}{{.Title.String}}{{else}}{{.ID}}{{end}} | {{.UpdatedFriendly}} |
{{.Message}}
{{end}} - {{if eq .Content.ID "about"}} -Describe what your instance is about. Accepts Markdown.
+Describe what your instance is about.
{{else if eq .Content.ID "privacy"}} -Outline your privacy policy. Accepts Markdown.
- {{else}} -Accepts Markdown and HTML.
+Outline your privacy policy.
{{end}} + {{if .Message}}{{.Message}}
{{end}} +